<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
</head>

<body>
    <div id="app">
        <input type="text" v-model='count'>
        {{count}}
    </div>
    <script>
        // 数组方法的拦截
        const arrayProto = [];
        const methodList = [
            "push",
            "pop",
            "unshift",
            "shift",
            "splice",
            "slice",
            "sort",
            "reverse",
        ];
        methodList.forEach((method) => {
            def(arrayProto, method, function () {
                // 调用原生的方法
                const result = Array.prototype[method].apply(this, arguments);
                switch (method) {
                    case "push": {
                        // 对参数添加的内容继续递归
                        observe(arguments);
                        // 通知依赖去响应
                        this.__ob__.dep.notify();
                        break;
                    }
                }
                return result;
            });
        });
        // 定义一个函数，专门给对象添加一个属性
        function def(obj, key, value, enumerable = false) {
            // 对Object.defindProperty进行封装
            Object.defineProperty(obj, key, {
                // 默认情况下，使用 Object.defineProperty() 添加的属性值是不可修改（immutable）的。
                // 该属性对应的值。可以是任何有效的 JavaScript 值（数值，对象，函数等）。
                // 默认为 undefined
                value,
                // 当且仅当该属性的 enumerable 键值为 true 时，该属性才会出现在对象的枚举属性中。
                // 默认为 false。
                enumerable,
                // 当且仅当该属性的 writable 键值为 true 时，属性的值，也就是上面的 value，才能被赋值运算符改变。
                // 默认为 false。
                writable: true,
                // 当且仅当该属性的 configurable 键值为 true 时，该属性的描述符才能够被改变，
                // 同时该属性也能从对应的对象上被删除。默认为 false。
                configurable: true,
            });
        }
        // 定义发布订阅
        class Dep {
            constructor() {
                // 定义订阅者列表
                this.subs = [];
            }
            // 添加订阅者
            append() {
                this.subs.push(Dep.target);
            }
            // 数据发生变化，通过观察者更新数据
            notify() {
                this.subs.forEach((sub) => {
                    sub.update();
                });
            }
        }
        // 定于观察者
        class Watcher {
            constructor(vm=null, obj, key) {
                // 为了确定在defindReactive方法里添加的观察者是谁，这里需要添加一个全局属性
                Dep.target = this;
                this.vm = vm
                this.node = obj;
                this.key = key;
                this.get()
                // 添加完成之后，需要清空
                Dep.target = null;
            }
            get() {
                // return this.value[this.key];
                this.value = this.vm.data[this.key]
                this.node.nodeValue = this.value
                return this.value[this.key];
            }
            // 观察者监测到属性值发生变化之后会更新页面模板
            update() {
                this.get();
                this.node.nodeValue = this.value
                console.log("监听" + this.key + "被改变了" + this.value);
            }
        }

        function defindReactive(obj, key) {
            // 获取值
            let value = obj[key];
            // 创建发布订阅中心对象
            let dep = new Dep();
            // 每一次都需要将值进行观察监测，直到值的属性是一个普通属性，即不是对象属性
            let childOb = observe(value);
            // 该方法有三个参数，分别是要定义属性的对象、要定义或者修改的属性的名称、属性描述（即实现get和set方法）
            // 对于Object.defineProperty的详细介绍的官网：https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
            Object.defineProperty(obj, key, {
                get() {
                    console.log("正在获取数据" + key);
                    // 有一个观察者在访问数据，需要将这个观察者存入订阅发布中心
                    if (Dep.target) {
                        dep.append();
                        if (childOb) {
                            childOb.dep.append();
                        }
                    }
                    // 这里一定要有return语句
                    return value;
                },
                // set方法是在属性值修改时被触发，所以这里需要接收一个参数，表示修改的新值
                set(newVal) {
                    console.log("正在修改数据：" + newVal);
                    if (value == newVal) {
                        return;
                    }
                    value = newVal;
                    // 更新后的值，可能是一个对象，这里需要调用observe
                    childOb = observe(newVal);
                    // 数据改变了，需要通知订阅者
                    dep.notify();
                },
            });
        }
        class Observer {
            // obj是从前面observe中传递过来的对象
            constructor(obj) {
                // 把Observer实例放在数据对象的__ob__属性上
                def(obj, "__ob__", this);
                this.dep = new Dep();
                // 保留数据，方便后面的处理
                this.value = obj;
                // 将obj中的对象属性和数组分别进行处理
                if (Array.isArray(obj)) {
                    // 如果是数组就可以拦截数组方法
                    this.value.__proto__ = arrayProto;
                    this.observeAray();
                } else {
                    this.walk();
                }
            }
            observeAray() {
                for (let i = 0; i < this.value.length; i++) {
                    // 递归处理，直到处理的属性是一个普通属性
                    observe(this.value[i], this.vm);
                }
            }
            walk() {
                // 把每个对象属性绑定一个getter属性和setter属性
                Object.keys(this.value).forEach((key) => {
                    defindReactive(this.value, key);
                });
            }
        }
        // 定义一个监测函数
        function observe(value, vm) {
            // 判断当前传递过来的参数是否是对象
            if (typeof value != "object") {
                // 什么也不做直接返回
                return;
            }
            let ob = value.__ob__;
            // 将对象再进行处理，监测每个属性的变化
            if (!ob) {
                ob = new Observer(value);
            }
            return ob;
        }

        //   将根节点上的每个子节点添加到碎片文档中
        function nodeToFragment(node, vm) {
            var fragment = document.createDocumentFragment()

            // console.log(node.children);
            var child
            // // 循环遍历取出每个子节点
            while (child = node.firstChild) {
                compile(child, vm)
                fragment.appendChild(child)
            }
            return fragment
        }

        //   定义一个模板编译函数
        function compile(node, vm) {
            // 匹配{{ }}语法
            var reg = /\{\{(.*)\}\}/
            // 如果是节点元素
            if (node.nodeType === 1) {
                var attrs = node.attributes
                for (let i = 0; i < attrs.length; i++) {
                    if (attrs[i].nodeName === 'v-model') {
                        var name = attrs[i].nodeValue
                        node.addEventListener('input', function (e) {
                            e = e || window.event
                            vm.data[name] = e.target.value
                        })
                        node.value = vm.data[name]
                        node.removeAttribute('v-model')
                    }
                }
            }
            // 如果是文本结点
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    // 获取匹配到的第一个字符串
                    var name = RegExp.$1
                    // 去掉前后空格
                    name = name.trim()
                    new Watcher(vm, node, name)
                }
            }
        }

        function Vue(options) {
            // 将options中的参数都添加到当前实例中
            Object.keys(options).forEach((key) => {
                this[key] = options[key];
            });

            // 处理data属性，将其变成可响应式的，即可监测的
            observe(this.data, this);
            var dom = nodeToFragment(document.getElementById(this.el), this)
            document.getElementById(this.el).appendChild(dom)

            // 可以查看一下
            // console.log(this);

            // new Watcher(this, "data");
            // new Watcher(this.data, "count");
            // new Watcher(this.data, "user");
            // new Watcher(this.data, "list");
        }
        // 新增一个对象属性或者修改数组里的某个下标
        Vue.$set = function (obj, key, value) {
            // 赋新值
            obj[key] = value;
            if (!Array.isArray(obj)) {
                defindReactive(obj, key);
            }
            obj.__ob__.dep.notify();
        };
        const vm = new Vue({
            el: 'app',
            data: {
                count: 123,
                user: {
                    name: "zs",
                    age: 18,
                },
                list: ["a", "b", "c"],
            },
            test() {
                Vue.$set(this.data.list, 3, "d");
                this.data.list.push("e");
            },
        });
          vm.test();
    </script>
</body>

</html>